﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using Castle.DynamicProxy;
using EntityAop.Configuration;
using EntityAop.Extensions;
using EntityAop.Interceptors;
using EntityAop.Interceptors.InterfacesToImplement;

namespace EntityAop
{
    public class ProxyGenerator : Castle.DynamicProxy.ProxyGenerator, IDisposable
    {
        protected virtual EntityAopConfiguration Configuration
        {
            get { return EntityAopConfiguration.Instance; }
        }

        private readonly Dictionary<string, IList<PropertyInfo>> CollectionPropertyInfosCache = new Dictionary<string, IList<PropertyInfo>>();
        private readonly Dictionary<int, IList<PropertyInfo>> EntityPropertiesCache = new Dictionary<int, IList<PropertyInfo>>();
        private readonly Dictionary<int, bool> TypesToBeProxied = new Dictionary<int, bool>();
        private readonly Dictionary<int, Type> GenericTypeCache = new Dictionary<int, Type>();

        public ProxyGenerator()
            : base()
        {
            Init();
        }

        public ProxyGenerator(IProxyBuilder proxyBuilder)
            : base(proxyBuilder)
        {
            Init();
        }

        private void Init()
        {
        }

        public T CreateClassProxyWithTargetRecursive<T>(T target, ProxyGenerationOptions options, IInterceptorWithRelatedTypes[] interceptors)
        {
            if (target == null)
            {
                return target;
            }

            Type targetType = target.GetType();

            if (target is ICollection)
            {
                return ProxyCollection(target, targetType, options, interceptors);
            }

            if (!IsTypeToBeProxied(targetType))
            {
                return target;
            }

            ProxyIEnumerableProperties(target, targetType, options, interceptors);

            ProxyEntityProperties(target, targetType
                , GetEntityPropertyInfos
                , (value) => CreateClassProxyWithTargetRecursive(value, options, interceptors));

            //Configuration.IsInProxyCreationPhase = true;

            T result;

            if (IsInstanceProxied(target))
            {
                result = target;
            }
            else
            {
                result = (T)CreateClassProxyWithTarget(targetType, GetAdditionalInterfaces(interceptors), target, options, interceptors);
            }

            //Configuration.IsInProxyCreationPhase = false;

            return result;
        }

        protected virtual IEnumerable<KeyValuePair<PropertyInfo, object>> GetPropertiesAndValues(Type type, params KeyValuePair<string, object>[] nameValues)
        {
            return nameValues.Select(
                nameValue => new KeyValuePair<PropertyInfo, object>(type.GetProperty(nameValue.Key), nameValue.Value))
                .ToList();
        }

        private Type[] GetAdditionalInterfaces(IInterceptorWithRelatedTypes[] interceptors)
        {
            List<Type> additionalInterfacesList = new List<Type>();

            foreach (var interceptor in interceptors.Where(interceptor => interceptor.RelatedTypes != null))
            {
                additionalInterfacesList.AddRange(interceptor.RelatedTypes);
            }

            return additionalInterfacesList.ToArray();
        }

        private T ProxyCollection<T>(T target, Type targetType, ProxyGenerationOptions options, IInterceptorWithRelatedTypes[] interceptors)
        {
            var observableCollection = CreateCollectionForProxy(targetType);

            foreach (var item in target as IEnumerable)
            {
                observableCollection.Add(CreateClassProxyWithTargetRecursive(item, options, interceptors));
            }

            return (T)observableCollection;
        }

        private void ProxyIEnumerableProperties(object target, Type targetType, ProxyGenerationOptions options, IInterceptorWithRelatedTypes[] interceptors)
        {
            IList<PropertyInfo> collectionPropertyInfos;

            if (CollectionPropertyInfosCache.ContainsKey(targetType.FullName))
            {
                collectionPropertyInfos = CollectionPropertyInfosCache[targetType.FullName];
            }
            else
            {
                collectionPropertyInfos = GetCollectionPropertyInfos(targetType);

                CollectionPropertyInfosCache.Add(targetType.FullName, collectionPropertyInfos);
            }

            foreach (var collectionPropertyInfo in collectionPropertyInfos)
            {
                if (!collectionPropertyInfo.CanWrite || collectionPropertyInfo.PropertyType.IsArray)
                {
                    continue;
                }

                var actualCollection = collectionPropertyInfo.GetValue(target, null);

                object observableCollection;

                if (actualCollection == null)
                {
                    observableCollection = CreateCollectionForProxy(collectionPropertyInfo.PropertyType);
                }
                else
                {
                    observableCollection = CreateClassProxyWithTargetRecursive(collectionPropertyInfo.GetValue(target, null), options, interceptors);
                }

                Configuration.IsInProxyCreationPhase = true;

                collectionPropertyInfo.SetValue(target, observableCollection, null);

                Configuration.IsInProxyCreationPhase = false;
            }
        }

        private IList<PropertyInfo> GetCollectionPropertyInfos(Type targetType)
        {
            return targetType.GetMembers()
                .Where(predicate => predicate.MemberType == MemberTypes.Property)
                .Cast<PropertyInfo>()
                .Where(predicate => predicate.PropertyType.IsIList())
                .ToList();
        }

        private IList CreateCollectionForProxy(Type type)
        {
            return CreateCollection(type, Configuration.TargetCollectionType);
        }

        private IList CreateCollectionForUnproxy(Type type)
        {
            return CreateCollection(type, typeof(List<>));
        }

        private IList CreateCollection(Type type, Type targetCollectionType)
        {
            Type genericArgument = null;

            if (type.IsArray && type.HasElementType)
            {
                genericArgument = type.GetElementType();
            }
            else if (type.IsGenericType)
            {
                genericArgument = type.GetGenericArguments()[0];
            }

            return CreateGeneric(targetCollectionType, genericArgument) as IList;
            
        }

        private void ProxyEntityProperties(object target, Type targetType, Func<Type, IList<PropertyInfo>> getPropertyInfos, Func<object, object> proxyFunc)
        {
            IList<PropertyInfo> propertyInfos;

            if (EntityPropertiesCache.ContainsKey(targetType.GetHashCode()))
            {
                propertyInfos = EntityPropertiesCache[targetType.GetHashCode()];
            }
            else
            {
                propertyInfos = getPropertyInfos(targetType);

                EntityPropertiesCache.Add(targetType.GetHashCode(), propertyInfos);
            }

            foreach (var propertyInfo in propertyInfos)
            {
                if (!propertyInfo.CanWrite)
                {
                    continue;
                }

                var proxiedPropertyValue = proxyFunc(propertyInfo.GetValue(target, null));

                Configuration.IsInProxyCreationPhase = true;
                propertyInfo.SetValue(target, proxiedPropertyValue, null);
                Configuration.IsInProxyCreationPhase = false;
            }
        }

        private IList<PropertyInfo> GetEntityPropertyInfos(Type targetType)
        {
            return targetType.GetMembers()
                .Where(predicate => predicate.MemberType == MemberTypes.Property)
                .Cast<PropertyInfo>()
                .Where(predicate => IsTypeToBeProxied(predicate.PropertyType))
                .ToList();
        }

        private bool IsTypeToBeProxied(Type type)
        {
            var key = type.FullName.GetHashCode();

            if (TypesToBeProxied.ContainsKey(key))
            {
                return TypesToBeProxied[key];
            }

            TypesToBeProxied.Add(key, type.IsClass && !type.IsSealed && !type.Namespace.StartsWith("System"));

            return TypesToBeProxied[key];
        }

        private object CreateGeneric(Type generic, Type innerType, params object[] args)
        {
            var key = (generic.FullName + innerType.FullName).GetHashCode();

            Type specificType;

            if (GenericTypeCache.ContainsKey(key))
            {
                specificType = GenericTypeCache[key];
            }
            else
            {
                specificType = generic.MakeGenericType(new[] {innerType});
                GenericTypeCache.Add(key, specificType);
            }
         
            return Activator.CreateInstance(specificType, args);
        }

        public void Dispose()
        {
            CollectionPropertyInfosCache.Clear();

            EntityPropertiesCache.Clear();
        }

        //public ICollection<T> Unproxy<T>(IList<T> proxiedInstance)
        //{
        //    if (proxiedInstance == null)
        //    {
        //        return proxiedInstance;
        //    }

        //    UnproxyOnCollectionHasBeenCalled = true;

        //    var result = (ICollection<T>)UnProxyCollection(proxiedInstance as IList);

        //    UnproxyOnCollectionHasBeenCalled = false;

        //    return result;
        //}

        public object Unproxy<T>(T proxiedInstance)
        {
            if (proxiedInstance == null)
            {
                return proxiedInstance;
            }

            Type proxyType = proxiedInstance.GetType();

            if (proxiedInstance is ICollection)
            {
                return UnProxyCollection(proxiedInstance as ICollection);
            }

            if (!IsTypeToBeProxied(proxyType))
            {
                return proxiedInstance;
            }

            ProxyEntityProperties(proxiedInstance, proxyType, GetEntityPropertyInfosForUnproxy, Unproxy);

            return GetTargetFieldValue(proxiedInstance);
        }

        public bool IsInstanceProxied(object instance)
        {
            return instance is IProxyTargetAccessor;
        }

        private IList<PropertyInfo> GetEntityPropertyInfosForUnproxy(Type targetType)
        {
            return targetType.GetMembers()
                .Where(predicate => predicate.MemberType == MemberTypes.Property)
                .Cast<PropertyInfo>()
                .Where(predicate => IsTypeToBeProxied(predicate.PropertyType) || predicate.PropertyType.IsInterface
                    //(predicate.PropertyType.IsClass || predicate.PropertyType.IsInterface) && (predicate.PropertyType.IsIList() || !predicate.PropertyType.Namespace.StartsWith("System"))
                    )
                .ToList();
        }

        private object GetTargetFieldValue(object value)
        {
            if (!IsInstanceProxied(value))
            {
                return value;
            }

            var valueAsTargetAccessor = value as ITargetAccessor;

            if (valueAsTargetAccessor != null)
            {
                return valueAsTargetAccessor.GetTarget();
            }

            var __targetField = value.GetType().GetField("__target");

            if (__targetField == null)
            {
                return value;
            }

            return __targetField.GetValue(value);
        }

        private IList UnProxyCollection(ICollection proxiedInstance)
        {
            var unProxiedCollection = CreateCollectionForUnproxy(proxiedInstance.GetType());

            foreach (var item in proxiedInstance)
            {
                unProxiedCollection.Add(Unproxy(item));
            }

            return unProxiedCollection;
        }

        public IInterceptorWithRelatedTypes[] GetInterceptors(object instance)
        {
            if (!IsInstanceProxied(instance))
            {
                return null;
            }

            return (IInterceptorWithRelatedTypes[])instance.GetType().GetField("__interceptors").GetValue(instance);
        }

    }
}
